test(cli): 钉住三个命令都跑 reference-integrity suite(#4384) - #4402
Merged
Conversation
…ite (#4384) #4394 removed the instance — `validateReadonlyFlowWrites` was hand-wired into `os validate` and `os compile` and never into `os lint`, so an `error`-level gate left `os lint` returning clean for stacks `os compile` refuses. It did not remove the failure MODE. A rule wired into two commands out of three produces no failing assertion anywhere: every command's own tests pass, the rule's unit tests pass, and the only symptom is the three commands disagreeing about the same stack. That is how the last one survived from #3425 until #4394, with the suite's own header naming it "the standing proof" of the drift the suite exists to end — a comment doing a test's job. The invariant worth pinning is `os lint` ⊇ `os compile`'s gate set: lint is the cheap pre-flight, compile is the gate, and a green lint followed by a red compile makes the pre-flight worthless — worse than worthless for an agent, whose remaining options are re-verifying everything or learning to distrust the signal. The guard asserts each command calls `validateReferenceIntegrity` and that none imports a suite member directly — the import is what a second call site starts with, so catching it closes the window between adding the import and adding the call. It scans source rather than spying, for the same reason lazy-deps.test.ts scans src: vitest inlines imports through its transform, so a module-cache probe cannot prove which symbols a command file actually reaches for. A third assertion keeps the other two from passing vacuously if the suite were emptied or its export renamed. Verified to FAIL on a reintroduced direct import in compile.ts, not merely to pass today. Test-only; no runtime or behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
This was referenced Jul 31, 2026
Closed
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…4340 后续) (#4417) * fix(lint,cli): os lint / os compile 不再放行一个 os validate 会拒绝的 react 页面 (#4340 后续) `validateReactPageProps` 只手工接在 `os validate` 上,`os lint` 和 `os compile` 从来没跑过它。在 showcase 的 react 页面植入一处 gating 违规 (`<ListView filters={['no_such_col','=',stage]}>` —— 谓词命中不了任何行, SqlDriver 吞掉 "no such column" 后返回 [],列表回空且与「本来就没数据」无法区分) 实测: os lint os compile os validate 修复前 exit 0 放行 exit 0 放行 exit 1 拒绝 修复后 exit 1 拒绝 exit 1 拒绝 exit 1 拒绝 #4340 之后这条规则已经是整个 react 页面表面唯一的字段解析闸门:<ListView> 的 columns/fields/sort/grouping/userFilters、<ObjectForm> 的 fields/initialValues/sections/subforms、record:* 一族(与元数据表面共用同一张 COMPONENT_FIELD_SPECS)、<ObjectChart> 的 aggregate/axes,以及 searchableFields。 所以漏接不是少几条警告 —— 而是这些绑定在 build 路径上完全没人看,包括其中会 gate 的那些。 现接入 REFERENCE_INTEGRITY_RULES,`os validate` 那处手工接线随之删除,三个命令的 答案由构造保证一致。这正是 suite 设立要终结的漂移(#3583 §5 D5),也是 validateReadonlyFlowWrites 在 #4394 里刚走过的同一条路:那次是「一张 map、 两个检查、两套命令集合」,这次是「一次 JSX parse、七个 rule id、一套命令集合」。 按 charter,react-prop-missing-required / react-prop-typo 是 shape 而非解析问题, 本不属于 suite。它们随行的理由与 validateActionBodyWrites 完全相同(其注释已写明): 落在同一次 parse 的同一份 source 上,拆成第二个 member 就要把每个 react 页面 parse 两遍,而改回手工接线正是 suite 要终结的那种漂移。同时更正了那条注释里 "The ONE member here that emits two rule ids" 的说法 —— 现在不止一个。 规则行为零变化:id、严重级、文案都不动;输入也不变(`os validate` 原本就传 result.data,suite 拿到的是同一个)。#4402 的接线守卫会在下次有人想再手工接一条 规则时直接报错——本次删除 validate.ts 的直接 import 也正是被它逼出来的。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012rK2McPogrTJBKdUsThrGs * docs: validating-metadata 反映 react 页面字段绑定现在三个命令都跑 本 PR 把 react 页面的字段绑定检查从「只在 os validate」变成「suite 成员,三个命令 都跑」,这份文档有三处随之失真 —— 而 docs-drift 的包级扇出恰好没点到它(它按 @objectstack/cli / @objectstack/lint 列了 20 份,漏了语义上唯一真正耦合的这份)。 - "one gate, two entry points" 表补一行 React page block field bindings(两列都 ✓)。 - 表下那句把 "JSX/React page sources" 整体算作 os validate 独有 —— 现在只有 「源码能否 parse」这个 SHAPE 闸门是独有的,里面的字段绑定属于 reference-integrity, suite 跑到哪它就跑到哪(含 os lint)。改写为分开陈述,并记下这里曾经的漂移与 #4384 的接线守卫。 - 示例 os validate 输出里的 `→ Checking React-source page props (ADR-0081)...` 随手工接线一并删除,已按真实 CLI 输出核对:示例里的每一行 step 现在都能在 showcase 上逐字复现(comm 比对为空)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012rK2McPogrTJBKdUsThrGs --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Aug 1, 2026
os-zhuang
added a commit
that referenced
this pull request
Aug 1, 2026
…ands (#4409) (#4445) * fix(cli): every author-time rule that can gate runs on all three commands (#4409) `os validate`, `os build` and `os lint` each hand-wired their own subset of the author-time rules. Nothing connected the three lists, so "which rules run here?" was answerable only by diffing three 800-line files by eye — and the answer drifted every time a rule landed. The audit found 23 of 26 rules running on some strict subset, nine of them able to emit `error`. The worst direction was the least obvious: `os build` — the command that PUBLISHES — was the weakest gate of the three. A flow whose expression approver does not parse (`approval-expression-invalid`) built and published green; only `os lint` stopped it, and CI usually runs the other two. This is the same failure mode's fifth appearance (#3583, #3782, #4384/#4394, #4402). Each earlier repair removed an instance and left the MODE: a rule's command coverage was whatever its author remembered to type, and forgetting was silent. #4402's guard could not catch the rest — it filtered on the current member names of one suite, so a rule hand-wired into two commands from outside that suite passed it without a word. Replace remembering with a table: - `packages/cli/src/lint/authoring-rules.ts` declares all 26 rules as data — tier (gating/advisory), which stack tier they read (pre-parse vs parsed), which commands run them, and a written reason for the one narrowing. All three commands consume it through `runAuthoringRules()`; the three command files shrink by ~1000 lines between them. - `authoring-rule-wiring.test.ts` upgrades the guard from a name list to a ratchet: a gating rule on fewer than three commands fails, a narrowed rule with no reason fails, a command that calls a rule directly fails, and an `advisory` claim is checked against the rule's own source so a gate cannot wear an advisory label to buy partial coverage. Remaining direct calls are listed with reasons in `DIRECT_CALL_RATCHET` / `LINT_IMPORT_RATCHET`. - `authoring-rule-command-parity.test.ts` proves the verdict, not just the wiring: one case per previously-blind gating rule, plus the issue's own repro driven through the real CLI — exit 1 on all three commands where it was 1/0/0 before. Two things fall out of one report per run rather than exiting at the first failing gate: an author with three unrelated problems sees all three, and `--strict` now covers every advisory instead of the roughly half that happened to be printed inline. Also closes the same hole one gate over: `collectAndLintDocs` failed `os build` and never ran on `os validate`, invisible because the parity guard keyed on the `lint*`/`validate*` naming convention and that gate is called `collect*`. The guard now names each shared non-registry gate explicitly. Cost is not what argued against this: the heavy deps (typescript, sucrase) are already lazy, and the heaviest rule of the set has run on all three commands as a suite member since #4340 without anyone noticing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz61GE1CWCSnGW4qShSEXs * chore(changeset): author-time rule command-coverage registry (#4409) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz61GE1CWCSnGW4qShSEXs * docs: name the ADR-0085 pointers by field, not by the reserved word (#4409) The role-word ratchet (ADR-0090 D3) counts occurrences per file, and two doc lines describing `validateSemanticRoles` reintroduced the banned word for a non-permission concept. Name what the rule actually checks instead: `stageField` / `highlightFields` pointers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz61GE1CWCSnGW4qShSEXs --------- Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang
pushed a commit
that referenced
this pull request
Aug 1, 2026
…om the other side (#4449) `validateFormLayout` was implemented, unit-tested, exported and given published rule ids — and no command ever called it. A whole-repo search found the implementation, the barrel export line and its own unit test, and nothing else: the rule ran on zero stacks for as long as it existed. Two changes: * register it in `AUTHORING_RULES` as `advisory` on all three commands. It walks structured metadata only (no lazy dependency), so `os validate`, `os build` and `os lint` pay nothing measurable for it. * add the reverse closure to the wiring guard. Every invariant #4409 shipped starts FROM a registry and looks at the commands, which cannot see a rule that never entered a registry — the same blind spot as #4402's name list, one layer up. The guard now subtracts both registries from the `validate*` / `lint*` symbols on `@objectstack/lint`'s public barrel; the difference must be empty or ledgered with a reason in `UNWIRED_RULE_LEDGER`, which ships empty because today's difference was exactly this one rule. The new tests fail without the registry entry: the closure reports `validateFormLayout` as unwired, and the liveness test asserts the entry's own `run` adapter returns both findings for a stack that earns them — membership alone is not evidence a rule produces output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 2, 2026
…objectstack-ai#4462, objectstack-ai#4432, objectstack-ai#4455, objectstack-ai#4449) (objectstack-ai#4520) * fix(cli,lint): run validateFormLayout, and close the rule registry from the other side (objectstack-ai#4449) `validateFormLayout` was implemented, unit-tested, exported and given published rule ids — and no command ever called it. A whole-repo search found the implementation, the barrel export line and its own unit test, and nothing else: the rule ran on zero stacks for as long as it existed. Two changes: * register it in `AUTHORING_RULES` as `advisory` on all three commands. It walks structured metadata only (no lazy dependency), so `os validate`, `os build` and `os lint` pay nothing measurable for it. * add the reverse closure to the wiring guard. Every invariant objectstack-ai#4409 shipped starts FROM a registry and looks at the commands, which cannot see a rule that never entered a registry — the same blind spot as objectstack-ai#4402's name list, one layer up. The guard now subtracts both registries from the `validate*` / `lint*` symbols on `@objectstack/lint`'s public barrel; the difference must be empty or ledgered with a reason in `UNWIRED_RULE_LEDGER`, which ships empty because today's difference was exactly this one rule. The new tests fail without the registry entry: the closure reports `validateFormLayout` as unwired, and the liveness test asserts the entry's own `run` adapter returns both findings for a stack that earns them — membership alone is not evidence a rule produces output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * fix(spec): a stored reference holding an embedded record is not a valid id (objectstack-ai#4455) `os migrate value-shapes` is the evidence half of the ADR-0104 D1 per-deployment gate, and the scan's own header names the case it exists for: "a `location` stored as `{latitude, longitude}` or a `lookup` holding an expanded record object". The second case was never detected. `ReferenceIdValueSchema` was `z.string().min(1)`, and in a SQL deployment a legacy embedded reference reaches storage as JSON TEXT — a non-empty string. So a deployment carrying exactly the values the gate exists to find ran the scan, was told it was clean, and closed the gate with `--apply`; and because the scan deliberately imports the write-path predicate, the write path was equally blind, so the value also survived future writes. `ReferenceIdValueSchema` now rejects a value whose first non-space character is `{` or `[`, in the expanded form too — `$expand` produces an object, never its serialization. Deliberately narrower than an id charset. `FileReferenceIdValueSchema` can bound its alphabet because a `sys_file` id is minted by the platform and nothing else; a reference id is whatever the target object's key holds, including an external key an ADR-0015 federated datasource supplies. So this rejects the shape that is provably not an id and leaves the alphabet to the object that owns it — `CB0-2026-0001`, `SFDC:001xx…` and `ops/eu-west/tenant-7` stay valid, and the tests pin that. Regression coverage is at the GATE, not just the schema: the scan test plants the serialized embedded record, asserts it is counted, and asserts `valueShapeScanPassed()` is false — the deployment may not record the flag — then asserts the same value is a write rejection under strict, so the scan and the validator still answer with one predicate. Reaches authors through the ADR-0104 warn-first path until a deployment opts into strict, so nothing starts rejecting writes on upgrade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * fix(metadata-protocol): one canonical type key at the /meta boundary (objectstack-ai#4432) objectstack-ai#3985 taught the per-type gates to accept both spellings of the `/meta` type segment. It did not FOLD them, so `/meta/actions/x` and `/meta/action/x` addressed two namespaces and the layers below disagreed about which one an item lived in — `SysMetadataRepository` folded to singular on its own, while the authorization tier above it (`isOverlayAllowed`, `isArtifactBacked`), the registry heal below it (`restoreArtifactRegistryView`) and the list hydration all read the caller's spelling. The damaging half was the hydration. `getMetaItems` registered overlay rows back into the SchemaRegistry under `request.type`, so one plural-spelled read minted a PLURAL registry entry; from the next read on `listItems('actions')` was no longer empty, the singular fallback that had been supplying every code-authored action stopped running, and one overlay row hid the entire code-authored listing — on a spelling no DELETE addresses, so it outlived the delete that was meant to lift it and left listing and dispatch disagreeing about a removed item. `saveMetaItem`, `getMetaItem`, `getMetaItems`, `getMetaItemLayered`, `getMetaItemCached` and `deleteMetaItem` now fold the type to its canonical singular as their first act. Reads of data AT REST keep the other-spelling fallback: rows written under a plural `type` before this fix are real and nothing rewrites them on upgrade. What changed is that nothing WRITES or REGISTERS a non-canonical key any more. Regression tests fail without the fold: a plural-spelled read mints a phantom `actions` registry entry and the second read drops the code-authored actions, and `getMetaItem` echoes back the caller's spelling so a client can round-trip it into a second namespace. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD * fix(objectql,service-datasource,runtime): a datasourceMapping rule is routing, not a hint (objectstack-ai#4462) Measured on `main` during the v17 verification: map an object to a Postgres datasource with a bad URL and the boot SUCCEEDS, `/ready` answers 200, the datasource name appears in ZERO log lines, the write returns 201 — and the row is physically in the DEFAULT store. The operator finds out by opening the database they declared and finding it empty. Two causes, one per layer, and each is what makes fixing the other correct: * `ObjectQLEngine.getDriver` step 2 read `mapped && drivers.has(mapped)`, so a MATCHED mapping rule naming a datasource with no live driver fell silently through to the default driver. It now throws — `DatasourceUnavailableError` when the connect layer recorded a verdict (objectstack-ai#3828), otherwise an error naming the object, the datasource and the two remedies. `default` still resolves onward: the default driver keeps its natural name (objectstack-ai#3826), so `drivers.has('default')` is false by construction and step 5 IS how routing to it works. * ADR-0062 D2's phase-1 note deliberately excluded "mapped" from the auto-connect gate, to keep `examples/app-crm` byte-for-byte unchanged. That note priced only one side. Gate (d) now fires when a mapping rule routes at least one object to a datasource, and a `declared-auto` failure is FATAL with an operator-readable reason — the same call gate (b) already makes for an explicit `object.datasource` binding, correct for (d) now that routing no longer supplies a fallback. `OS_ALLOW_DRIVER_CONNECT_FAILURE` still degrades. The mapped-object list comes from the engine's own matcher (`ObjectQLEngine.resolveMappedDatasource`, newly public) via `connectDeclared({ mappedObjects })`. The connection service never re-derives rule matching: two matchers drifting by one clause would connect a datasource routing never uses, or route to one nothing connects — the defect again. `examples/app-crm`'s mapping is DELETED, and that is what keeps the example unchanged rather than what breaks it. Its `namespace: 'crm'` rule never matched (`namespace` is deprecated; no object sets it) and its `default: true → crm_primary` rule routed everything to an unconnected `:memory:` datasource, i.e. to the default store by fall-through. Honouring it would move the whole app — platform objects included — onto a database empty on every boot. Verified against a real boot on a private port, not only in unit tests: * unchanged CRM example boots healthy; crm_primary/crm_analytics stay `unvalidated` (metadata-only) exactly as before; * with a mapping to `postgres://…@127.0.0.1:1/nonexistent_db`, boot exits 1 with "1 object(s) are routed to it by a datasourceMapping rule (crm_account) and have no fallback datasource — their reads/writes would otherwise land in a DIFFERENT database than the one they declare ⇒ fail-fast per ADR-0062 D5"; * under OS_ALLOW_DRIVER_CONNECT_FAILURE=1 the degraded-boot banner carries the same sentence and the mapped object's seeds fail instead of silently populating the default store. ADR-0062 D2 carries the amendment; the docs page and the data skill now state that a mapping rule is routing and fails the boot when it cannot be honoured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4384
这个 PR 补的是什么
#4394 已经修掉了那一例 ——
validateReadonlyFlowWrites手工接进os validate和os compile,唯独漏了os lint,而它是 error 级,于是os lint长期放行os compile会拒绝的 stack。但它没有修掉失败模式本身:今天仍然没有任何东西能阻止下一条规则被接进三个命令里的两个。
而这类缺陷的特点是不会有任何测试变红 —— 每个命令自己的测试都过,规则的单测也过,唯一症状就是三个命令对同一个 stack 给出不同答案。上一次就是这么从 #3425 一直活到 #4394 的,期间 suite 头注释里把它称作 "the standing proof of the drift the suite exists to end" —— 一条注释在干测试的活。
钉住的不变量
os lint⊇os compile的门禁集。lint 是廉价预检,compile 是门禁。lint 绿、compile 红,预检就没有存在意义;对 agent 更糟 —— 剩下的选择只有"全量重验"(慢)或"学会不信任这个信号"(危险)。
三条断言
validateReferenceIntegrity;validateReadonlyFlowWrites—— 防止前两条在 suite 被清空或导出改名后空转通过。为什么扫源码而不是打桩
vitest 会把 import 内联进 transform,所以对
@objectstack/lint打 spy 证明不了命令文件到底 reach 了哪些符号 —— 和lazy-deps.test.ts扫src/是同一个理由。suite 内容的行为覆盖在
@objectstack/lint的reference-integrity-suite.test.ts;本文件只守 suite 与三个调用点之间那道缝。反向验证
不是"今天能过"就算数。在
compile.ts里加回一行直接 import,它确实失败:范围
纯测试,无运行时/行为变更。
@objectstack/cli65 文件 / 636 测试全绿,typecheck干净。空 changeset。顺带说明为什么只提这一条:我原本按 #4384 的分析做了一版"家族 + 组合层"(把 suite 的成员判据和"哪些命令跑"拆开),做到一半发现 #4394 已经先合入了。实测确认症状已消失(同一 fixture 下
os lint现在 exit 1),所以那版重构的核心价值归零,我 abort 了。剩下一个可以讨论但不在本 PR 范围的点:
validateReadonlyFlowWrites问的是"名字解析得到、但写会不会生效",而 suite 的判据写的是"名字能否解析"。#4394 的理由是"它走的是同一张 map"——这是邻近性论证。要不要为此调整 suite 的判据表述,是维护者的取舍,我没有在这里动它。🤖 Generated with Claude Code